import {reactive, shallowReactive} from "./reactivity";
import type {ComponentPropsOptions, PropConstructor} from "./prop.type";
import {useMemo, useRef} from "react";

/**
 * 转化并且校验props
 * @author  韦胜健
 * @date    2021/3/9 18:16
 */
export const useSetupProps = (() => {

    function getTypeName(type: any): string {
        if (Array.isArray(type)) {
            return type.map(t => getTypeName(t)).join(', ')
        } else if (type === true) {
            return 'true'
        } else if (type === false) {
            return 'false'
        } else if (typeof type === "function") {
            return type.name
        }
        return 'unknown'
    }

    function emitCheckError(propName: string, expectType: any, actualType: any, value: any) {
        console.log({propName, expectType, actualType, value})
        console.warn(`Invalid prop: type check failed for prop "${propName}". Expected ${getTypeName(expectType)}, got ${getTypeName(actualType)} with value ${value}.`)
    }

    function emitRequiredError(propName: string) {
        console.warn(`Missing required prop: "${propName}"`)
    }

    function checkPropOption(val: any, prop: any, propName: string) {
        if (!prop) return
        if (val === undefined) {
            if (typeof prop === "object" && !Array.isArray(prop) && prop.required) {
                emitRequiredError(propName)
            }
            return;
        }
        if (val === null) {
            return;
        }

        let types: (PropConstructor | true)[] = []
        let type = typeof prop === "object" && !Array.isArray(prop) ? prop.type : prop
        if (typeof type === "function") {
            types = [type]
        } else if (Array.isArray(type)) {
            types = type
        } else if (type === true) {
            types = [true]
        }
        if (types.length === 0) {return;}
        const fit = types.find(t => {
            if (t === true) {return val === true}
            if (t === Function) {return typeof val === "function" || val instanceof Function}
            return val.__proto__.constructor === t || val.__proto__.constructor instanceof t
        })
        // console.log('fit', {fit, types, val})
        if (fit == null) {
            emitCheckError(propName, types, val.__proto__.constructor, val)
        }
    }

    return (
        {
            fcProps,
            emitOptions,
            propOptions,
            slotOptions,
            scopeSlotOptions,
        }: {
            fcProps: Record<string, any>,
            emitOptions: undefined | Record<string, any>,
            propOptions: undefined | Readonly<ComponentPropsOptions>,
            slotOptions: undefined | string[],
            scopeSlotOptions: undefined | Record<string, any>,
        }) => {

        /*计算不被继承的属性名数组*/
        const notInheritAttrNames = useMemo(() => {
            const names: string[] = []
            if (!!emitOptions) {
                const emitPropNames = Object.keys(emitOptions)
                names.push(...emitPropNames)
                if (emitPropNames.indexOf('onUpdateModelValue') > -1 && emitPropNames.indexOf('onChange') === -1) {
                    names.push('onChange')
                }
            }
            if (!!propOptions) {
                if (Array.isArray(propOptions)) {
                    names.push(...propOptions)
                } else {
                    names.push(...Object.keys(propOptions))
                }
            }
            if (!!slotOptions) {
                names.push(...slotOptions)
            }
            if (!!scopeSlotOptions) {
                names.push(...Object.keys(scopeSlotOptions))
            }
            names.push(...[
                'children',
                'ref',
                'onRef',
                'onBeforeMount',
                'onBeforeUnmount',
                'onBeforeUpdate',
                'onMounted',
                'onUnmounted',
                'onUpdated',
            ])
            return names
        }, [])

        /*
        *  props: 不被继承的属性
        *  attrs： 不处理的属性，需要被继承
        */
        const {props, attrs} = useMemo(() => {
            const props = {} as Record<string, any>
            const attrs = {} as Record<string, any>
            Object.keys(fcProps).forEach(propName => (notInheritAttrNames.indexOf(propName) > -1 ? props : attrs)[propName] = fcProps[propName])
            return {props, attrs}
        }, [fcProps])

        const prevProps = useRef(null as null | Record<string, any>)
        const curProps = useRef({} as Record<string, any>)

        if (!!propOptions) {
            if (Array.isArray(propOptions)) {
                // propOptions是一个字符串数组，字符串就是接收的 prop name数组，此时认为所有prop name为任意运行时类型
                // 不检查fcProps中值得类型
                propOptions.forEach(propName => curProps.current[propName] = props[propName])
            } else {
                Object.entries(propOptions).forEach(([propName, propOption]) => {
                    if (!!prevProps.current && prevProps.current[propName] === props[propName]) {
                        /*如果上一次render的fcProps，与当前fcProps的propName值相同，就不修改props的propName值*/
                        return
                    }
                    let val = props[propName]
                    if (val === undefined || curProps.current[propName] !== val) {
                        /*console.log('check prop', {val, propName, propOption,})*/
                        checkPropOption(val, propOption, propName)
                    }
                    if (val === undefined && !!propOption) {
                        if ('default' in propOption && propOption.default !== undefined) {
                            if (typeof propOption.default === "function") {
                                if (propOption.type === Function) {
                                    val = propOption.default
                                } else {
                                    val = propOption.default()
                                }
                            } else {
                                val = propOption.default
                            }
                        } else if ('type' in propOption && (propOption.type === Boolean || (Array.isArray(propOption.type) && propOption.type.indexOf(Boolean) > -1))) {
                            val = false
                        }
                    }
                    curProps.current[propName] = val
                })
            }
        }
        prevProps.current = props
        const setupProps = useMemo(() => shallowReactive({...curProps.current}), [])

        if (!!propOptions) {
            if (Array.isArray(propOptions)) {
                propOptions.forEach((propName: string) => {
                    if (setupProps[propName] !== curProps.current[propName]) {
                        setupProps[propName] = curProps.current[propName]
                    }
                })
            } else {
                Object.keys(propOptions).forEach((propName: string) => {
                    if (setupProps[propName] !== curProps.current[propName]) {
                        setupProps[propName] = curProps.current[propName]
                    }
                })
            }
        }
        const attrState = useMemo(() => reactive({attrs}), [])
        Object.keys({...attrState.attrs, ...attrs}).forEach(attrName => {
            if (attrState.attrs[attrName] !== attrs[attrName]) {
                if (attrName in attrs) {
                    attrState.attrs[attrName] = attrs[attrName]
                } else {
                    delete attrState.attrs[attrName]
                }
            }
        })

        return {
            setupProps: setupProps as any,
            setupAttrs: attrState.attrs as any,
        }
    }
})();

const listenerPatten = /^on[A-Z].*/

export function mergeProps(
    {
        attrs,
        child,
    }: {
        child: any,
        attrs: any,
    }) {

    if (child == null || typeof child != "object" || Array.isArray(child)) {
        return child
    }

    let childProps = {...(child.props || {})}
    Object.entries(attrs).forEach(([attrName, attrValue]) => {
        if (attrValue === undefined) return;
        if (attrName === 'className') {
            childProps.className = !childProps.className ? attrValue : [childProps.className, attrValue].join(' ')
        } else if (attrName === 'style') {
            childProps.style = Object.assign({}, childProps.style, attrValue)
        } else if (listenerPatten.test(attrName)) {
            childProps[attrName] = !childProps[attrName] ? attrValue : ((...args: any[]) => {
                childProps[attrName](...args);
                (attrValue as any)(...args);
            })
        } else {
            childProps[attrName] = attrValue
        }
    })
    return {...child, props: childProps}
}

export function checkConflictProps(
    {
        propsOptions,
        emitOptions,
        slotOptions,
        scopeSlotOptions,
    }: {
        propsOptions: any,
        emitOptions: any,
        slotOptions: any,
        scopeSlotOptions: any,
    }) {
    if (!propsOptions) return
    Object.keys(propsOptions).forEach(propName => {
        if (!!emitOptions && !!emitOptions[propName]) {
            console.error(`prop name "${propName}" in props and emits at the same time!`)
        }
        if (!!slotOptions && slotOptions.indexOf(propName) > -1) {
            console.error(`prop name "${propName}" in props and slots at the same time!`)
        }
        if (!!scopeSlotOptions && !!scopeSlotOptions[propName]) {
            console.error(`prop name "${propName}" in props and scopeSlots at the same time!`)
        }
    })
}
